/***************************************************************************
 * Copyright 2012-2013 TXT e-solutions SpA
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 * 
 * This work was performed within the IoT_at_Work Project
 * and partially funded by the European Commission's
 * 7th Framework Programme under the contract ICT-257367.
 *
 * Authors:
 *      Salvatore Piccione (TXT e-solutions SpA)
 *
 * Contributors:
 *        Domenico Rotondi (TXT e-solutions SpA)
 **************************************************************************/
package it.txt.ens.client.impl;

import it.txt.access.capability.commons.signer.X509DocumentSigner;
import it.txt.access.capability.commons.signer.model.X509CertificateKeyValues;
import it.txt.access.capability.commons.signer.model.X509CertificateSubjectInfo;
import it.txt.access.capability.commons.signer.model.X509DocumentSignerInfo;
import it.txt.access.capability.commons.utils.XMLPrinter;
import it.txt.access.capability.finder.CapabilitySearchReturn;
import it.txt.access.capability.finder.CapabilityXQuerySaxURISearch;
import it.txt.access.capability.finder.util.URIResourceIDSections;
import it.txt.ens.client.core.ENSBrokerConnectionParameters;
import it.txt.ens.client.core.ENSClient;
import it.txt.ens.client.core.factory.ENSBrokerConnectionParametersFactory;
import it.txt.ens.client.exception.ENSClientException;
import it.txt.ens.client.exception.ENSClientExceptionCodes;
import it.txt.ens.core.ENSAuthzServiceConnectionParameters;
import it.txt.ens.core.ENSOperation;
import it.txt.ens.core.ENSResource;
import it.txt.ens.core.ENSStatus;
import it.txt.ens.core.KeystoreParameters;
import it.txt.ens.core.X509CertificateRetrievalParameters;
import it.txt.ens.core.exception.ENSConnectionException;
import it.txt.ens.core.exception.ENSConnectionExceptionCodes;
import it.txt.ens.schema.FailureResponseType;
import it.txt.ens.schema.RequestType;
import it.txt.ens.schema.ResponseType;
import it.txt.ens.schema.SuccessResponseType;
import it.txt.ens.schema.factory.ENSRequestFactoryException;
import it.txt.ens.schema.factory.ENSResponseFactoryException;
import it.txt.ens.schema.request.factory.ENSAuthorisationRequestFactory;
import it.txt.ens.schema.response.factory.ENSAuthorisationResponseFactory;

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.net.URISyntaxException;
import java.security.GeneralSecurityException;
import java.text.MessageFormat;
import java.util.Collections;
import java.util.Comparator;
import java.util.Date;
import java.util.List;
import java.util.Locale;
import java.util.ResourceBundle;
import java.util.UUID;
import java.util.concurrent.TimeoutException;
import java.util.logging.Level;
import java.util.logging.Logger;

import javax.xml.datatype.XMLGregorianCalendar;
import javax.xml.transform.TransformerException;

import org.w3c.dom.Document;

import com.rabbitmq.client.Channel;
import com.rabbitmq.client.Connection;
import com.rabbitmq.client.ConnectionFactory;
import com.rabbitmq.client.RpcClient;
import com.rabbitmq.client.ShutdownSignalException;

/**
 * Defines an abstract implementation of the {@link ENSClient} interface.<br/>
 * This implementation is abstract because a generic client is useless because can
 * only connect and disconnect to the ENS but cannot use the session. Subclasses (i.e.
 * classes that models publishers and subscribers) should use the operative broker's
 * connection parameters to connect to ENS resource they have been authorised to access
 * and publish or subscribe to an event stream.
 * 
 * @author Salvatore Piccione (TXT e-solutions SpA - salvatore.piccione AT txtgroup.com)
 * @author Domenico Rotondi (TXT e-solutions SpA - domenico.rotondi AT txtgroup.com)
 *
 */
public abstract class BasicENSClient implements ENSClient {
    
    /* (non-Javadoc)
     * @see it.txt.ens.client.core.ENSClient#getTargetResource()
     */
    @Override
    public ENSResource getTargetResource() {
        return targetResource;
    }

    private static final String BUNDLE_LOCATION = "resource-bundles/" + 
        BasicENSClient.class.getSimpleName();
    private static final ResourceBundle LOG_MESSAGES = ResourceBundle.getBundle(BUNDLE_LOCATION, Locale.ROOT);
    private static final Logger LOGGER = Logger.getLogger(BasicENSClient.class.getName(),
            BUNDLE_LOCATION);
    private static final String ENCODING = "UTF-8";
    private static final int TIMEOUT = 100000;
    /**
     * The AMQP connection to the operative broker.
     */
    protected Connection connection;
    /**
     * The AMQP channel to the operative broker.
     */
    protected Channel channel;
    /**
     * The AMQP connection factory.
     */
    protected ConnectionFactory factory;
    private final File capabilityDirectory;
    private final String subjectID;
    /**
     * The ENS resource the client has been authorised to access.
     */
    protected final ENSResource targetResource;
    private final ENSAuthorisationRequestFactory requestFactory;
    private final ENSAuthorisationResponseFactory responseFactory;
    private final ENSOperation operation;
    private final ENSAuthzServiceConnectionParameters authzServiceConnParams;
    private final X509CertificateRetrievalParameters certParams;
    private final KeystoreParameters keystoreParams;
    private final ENSBrokerConnectionParametersFactory connParamsFactory;
    private final Date sessionExpiration;
    private final String debugID;

    /**
     * Sets the fields of this objects.
     * 
     * @param subjectID
     * @param targetResource
     * @param operation
     * @param sessionExpiration
     * @param authzServiceConnParams
     * @param keystoreParams
     * @param certParams
     * @param capabilityDirectory
     * @param requestFactory
     * @param responseFactory
     * @param connParamsFactory
     */
    protected BasicENSClient (String subjectID, ENSResource targetResource, ENSOperation operation,
            Date sessionExpiration, ENSAuthzServiceConnectionParameters authzServiceConnParams,
            KeystoreParameters keystoreParams, X509CertificateRetrievalParameters certParams,
            File capabilityDirectory, ENSAuthorisationRequestFactory requestFactory,
            ENSAuthorisationResponseFactory responseFactory, ENSBrokerConnectionParametersFactory connParamsFactory) {
        
        this.capabilityDirectory = capabilityDirectory;
        this.targetResource = targetResource;
        this.subjectID = subjectID;
        this.requestFactory = requestFactory;
        this.responseFactory = responseFactory;
        this.operation = operation;
        this.authzServiceConnParams = authzServiceConnParams;
        this.certParams = certParams;
        this.keystoreParams = keystoreParams;
        this.connParamsFactory = connParamsFactory;
        this.sessionExpiration = sessionExpiration;
        this.debugID = this.getClass().getSimpleName() + UUID.randomUUID().toString();
    }
    
    /* (non-Javadoc)
     * @see it.txt.ens.client.core.ENSClient#connect()
     */
    @Override
    public void connect() throws ENSClientException, ENSConnectionException {
        //create and configure the RabbitMQ Connection Factory
        factory = new ConnectionFactory();
        factory.setUsername(authzServiceConnParams.getSubjectID());
        factory.setPassword(authzServiceConnParams.getAccessToken());
        factory.setPort(authzServiceConnParams.getBrokerPort());
        factory.setHost(authzServiceConnParams.getBrokerHost());
        factory.setVirtualHost(authzServiceConnParams.getVirtualHost());
          
        if (LOGGER.isLoggable(Level.FINE)) {
            LOGGER.log(Level.FINE, "authorisation.step1", 
                new Object[] {capabilityDirectory.getAbsoluteFile(), subjectID, targetResource.getURI().toString(),
                operation.toString()});
        }
        
        //START CAPABILITY SEARCH
        //retrieve the best suitable capabilities
        CapabilityXQuerySaxURISearch capabilityFinder = new CapabilityXQuerySaxURISearch(capabilityDirectory);
        
        URIResourceIDSections uriSections = new URIResourceIDSections();
        uriSections.setAuthority(targetResource.getHost());
        uriSections.setScheme(ENSResource.URI_SCHEME);
        uriSections.setNamespace(targetResource.getNamespace());
        uriSections.setPattern(targetResource.getPattern());
        uriSections.setService(targetResource.getPath());
        List<CapabilitySearchReturn> capabilities;
        try {
            capabilities = capabilityFinder.doSearchCapability(subjectID, 
                    uriSections.toURI().toString(), operation.toString());
        } catch (UnsupportedEncodingException e) {
            ENSClientException ece = new ENSClientException(
                ENSClientExceptionCodes.TARGET_RESOURCE_URI_CREATION_ERROR, e);
            LOGGER.log(Level.SEVERE, ece.getMessage(), e);
            throw ece;
        } catch (URISyntaxException e) {
            ENSClientException ece = new ENSClientException(
                ENSClientExceptionCodes.TARGET_RESOURCE_URI_CREATION_ERROR, e);
            LOGGER.log(Level.SEVERE, ece.getMessage(), e);
            throw ece;
        }
        if (LOGGER.isLoggable(Level.FINE)) {
            LOGGER.log(Level.FINE, "capabilitySearch.selectedCapabilities",
                capabilities.size());
        }
        if (capabilities.isEmpty())
            throw new ENSClientException(ENSClientExceptionCodes.NO_SUITABLE_CAPABILITY_FOUND);
        if (capabilities.size() > 1)
            Collections.sort(capabilities, new Comparator<CapabilitySearchReturn>() {
                public int compare(CapabilitySearchReturn o1, CapabilitySearchReturn o2) {
                    XMLGregorianCalendar issueDate1 = o1.getCapabilityIssueDateToXmlGregorianCalendar();
                    XMLGregorianCalendar isssueDate2 = o2.getCapabilityIssueDateToXmlGregorianCalendar();
                    return issueDate1.compare(isssueDate2);
                }
            });
        CapabilitySearchReturn selectedCapability = capabilities.get(0);
        
        if (LOGGER.isLoggable(Level.FINE)) {
            LOGGER.log(Level.FINE, "authorisation.step1OK",
                new Object[]{selectedCapability.getCapabilityID(),
                selectedCapability.getCapabilityIssueDateToXmlGregorianCalendar().toGregorianCalendar().getTime(),
                selectedCapability.getCapabilityFile().getAbsolutePath()});
            LOGGER.log(Level.FINE, "authorisation.step2");
        }
        //STOP CAPABILITY SEARCH
        
        //create a JAXB request object
        FileInputStream capabilityStream = null;
        RequestType requestType = null;
        try {
            capabilityStream = new FileInputStream(selectedCapability.getCapabilityFile());
            requestType = requestFactory.create(targetResource.getURI(), subjectID,
                    operation.toString(), sessionExpiration, capabilityStream);
        } catch (FileNotFoundException e) {
            if (LOGGER.isLoggable(Level.SEVERE))
                    LOGGER.log(Level.SEVERE, MessageFormat.format(
                            LOG_MESSAGES.getString("capabilitySearch.missingExistingFile"),
                            selectedCapability.getCapabilityFile().getAbsolutePath()), e);
            throw new ENSClientException(ENSClientExceptionCodes.MISSING_SELECTED_CAPABILITY, e);
        } catch (ENSRequestFactoryException e) {
            ENSClientException clientExc = new ENSClientException(
                    ENSClientExceptionCodes.REQUEST_CREATION, e);
            if (LOGGER.isLoggable(Level.SEVERE))
                LOGGER.log(Level.SEVERE, clientExc.getMessage(), e);
            throw clientExc;
        }
        
        //here we are sure that the request type has been instantiated
        Document requestDOM = null;
        try {
            requestDOM = requestFactory.marshal(requestType);
        } catch (ENSRequestFactoryException e) {
            ENSClientException clientExc = new ENSClientException(
                ENSClientExceptionCodes.REQUEST_MARSHALLING_ERROR, e);
            if (LOGGER.isLoggable(Level.SEVERE))
                LOGGER.log(Level.SEVERE, clientExc.getMessage(), e);
            throw clientExc;
        }
        //we are sure that the request DOM has been instantiated
        if (LOGGER.isLoggable(Level.FINE)) {
            LOGGER.log(Level.FINE, "authorisation.step2OK", XMLPrinter.printDocumentElement(
                requestDOM.getDocumentElement(), true));
            LOGGER.log(Level.FINE, "authorisation.step3");
        }
        
        X509DocumentSignerInfo signerInfo = new X509DocumentSignerInfo();
        signerInfo.setKeystorePath(keystoreParams.getKeystorePath().getAbsolutePath());
        signerInfo.setKeystorePwd(keystoreParams.getKeystorePassword());
        signerInfo.setPrivateKeyPwd(certParams.getPrivateKeyPassword());
        signerInfo.setSignerID(certParams.getX509CertificateSubject());
        try {
            X509CertificateKeyValues keyValues = X509DocumentSigner.getCertificateKeyValues(signerInfo);
            X509DocumentSigner.signXMLElement(requestDOM.getDocumentElement(), keyValues,
                    ENSAuthorisationRequestFactory.SIGN_DOCUMENT_AFTER_NODE);
        } catch (GeneralSecurityException e) {
            ENSClientException clientExc = new ENSClientException(ENSClientExceptionCodes.DIGITAL_SIGNATURE_CREATION_ERROR,e);
            if (LOGGER.isLoggable(Level.SEVERE))
                LOGGER.log(Level.SEVERE, clientExc.getMessage(), e);
            throw clientExc;
        }
        if (LOGGER.isLoggable(Level.FINE))
            LOGGER.log(Level.FINE, "authorisation.step3OK", 
                XMLPrinter.printDocumentElement(requestDOM.getDocumentElement(), true));
        
        //transformation of the digitally signed XML DOM into an array of byte
        ByteArrayOutputStream xmlos;
        
        try {
            xmlos = (ByteArrayOutputStream) XMLPrinter.convertDOMIntoByteStream(
                    requestDOM, false, null, new ByteArrayOutputStream()).getOutputStream();
        } catch (TransformerException e) {
            ENSClientException clientExc = new ENSClientException(ENSClientExceptionCodes.SIGNATURE_TO_BYTES_ERROR, e);
            if (LOGGER.isLoggable(Level.SEVERE))
                LOGGER.log(Level.SEVERE, clientExc.getMessage(), e);
            throw clientExc;
        }
        
        
        if (LOGGER.isLoggable(Level.FINE))
            LOGGER.log(Level.FINE, "authorisation.step4");
        Connection authorisationConnection = null;
        Channel authorisationChannel = null;
        String rawResponse = null;
        try {
            //initialise the connection to the ENS access request broker
            authorisationConnection = factory.newConnection();
            authorisationChannel = authorisationConnection.createChannel();
            
            //create an RPC Client
            //FIXME SHOULD WE INDICATE THE EXCHANGE??
            RpcClient client = new RpcClient(authorisationChannel, "", authzServiceConnParams.getDestinationName(),TIMEOUT);
            rawResponse = client.stringCall(xmlos.toString(ENCODING));
//            rawResponse = client.stringCall(xmlos.toString());
        } catch (IOException e) {
            ENSConnectionException connExc = new ENSConnectionException(
                ENSConnectionExceptionCodes.ACCESS_REQUEST_BROKER_CONNECTION_ERROR, e);
            if (LOGGER.isLoggable(Level.SEVERE))
                LOGGER.log(Level.SEVERE, connExc.getMessage(), e);
            throw connExc;
        } catch (ShutdownSignalException e) {
            ENSConnectionException connExc = new ENSConnectionException(
                ENSConnectionExceptionCodes.ACCESS_REQUEST_BROKER_CONNECTION_ERROR, e);
            if (LOGGER.isLoggable(Level.SEVERE))
                LOGGER.log(Level.SEVERE, connExc.getMessage(), e);
            throw connExc;
        } catch (TimeoutException e) {
            ENSConnectionException connExc = new ENSConnectionException(
                ENSConnectionExceptionCodes.ACCESS_REQUEST_BROKER_CONNECTION_ERROR, e);
            if (LOGGER.isLoggable(Level.SEVERE))
                LOGGER.log(Level.SEVERE, connExc.getMessage(), e);
            throw connExc;
        } finally {
            if (authorisationChannel != null)
                try {
                    authorisationChannel.close();
                } catch (IOException e) {}

            if (authorisationConnection != null)
                try {
                    authorisationConnection.close();
                } catch (IOException e) {}
        }
        
        if (LOGGER.isLoggable(Level.FINE)) {
            LOGGER.log(Level.FINE, "authorisation.step4OK",rawResponse);
        }
        if(ENSStatus.FailureCodes.INTERNAL_ERROR.equalsIgnoreCase(rawResponse)){
            throw new ENSClientException(ENSClientExceptionCodes.INTERNAL_SERVER_ERROR);
        }
        
        ResponseType responseObject = null;
        ByteArrayInputStream inputStream = null;
        try {
            inputStream = new ByteArrayInputStream(rawResponse.getBytes(ENCODING));
//            inputStream = new ByteArrayInputStream(rawResponse.getBytes());
            responseObject = responseFactory.parseInputStream(inputStream);
            Document responseDOM = responseFactory.marshal(responseObject);
                        
            if (LOGGER.isLoggable(Level.FINE)) {
                LOGGER.log(Level.FINE, "authorisation.step5");
            }
            boolean isSignatureVerified = X509DocumentSigner.verifyXMLElementSign(responseDOM.getDocumentElement(), new X509CertificateSubjectInfo());
            if (!isSignatureVerified) {
                throw new ENSClientException(ENSClientExceptionCodes.TAMPERED_RESPONSE);
            }
            
        } catch (UnsupportedEncodingException e) {
            ENSClientException ece = new ENSClientException(ENSClientExceptionCodes.UNSUPPORTED_ENCODING,e,ENCODING,debugID);
            if (LOGGER.isLoggable(Level.SEVERE))
                LOGGER.log(Level.SEVERE, ece.getMessage(), e);
            throw ece;
        } catch (ENSResponseFactoryException e) {
            ENSClientException ece = new ENSClientException(ENSClientExceptionCodes.RESPONSE_MARSHALLING_ERROR,e);
            if (LOGGER.isLoggable(Level.SEVERE))
                LOGGER.log(Level.SEVERE, ece.getMessage(), e);
            throw ece;
        } catch (GeneralSecurityException e) {
            ENSClientException ece = new ENSClientException(
                    ENSClientExceptionCodes.DIGITAL_SIGNATURE_VERIFICATION_ERROR,e);
            if (LOGGER.isLoggable(Level.SEVERE))
                LOGGER.log(Level.SEVERE, ece.getMessage(), e);
            throw ece;
        }
        
        if (LOGGER.isLoggable(Level.FINE)) {
            LOGGER.log(Level.FINE, "authorisation.step5OK");
            LOGGER.log(Level.FINE, "authorisation.step6");
        }
        
        //analysis of the response
        if (responseObject.isResult()) {
            SuccessResponseType success = responseObject.getResultDetails().getSuccessResponse();
            ENSBrokerConnectionParameters operativeBrokerConnParams = connParamsFactory.create(
                   success.getSubject().getSubjectID(), success.getAccessToken(), success.getBrokerHost(),
                   success.getBrokerPort().intValue(),success.getVirtualHost(), success.getQueueName(),
                   success.isUseTLS(), success.getSessionExpiration().toGregorianCalendar().getTime(),
                   success.getSessionToken());
            if (LOGGER.isLoggable(Level.INFO)) {
                LOGGER.log(Level.INFO, "authorisation.step6OK", new String[] {debugID,operativeBrokerConnParams.toString()});
            }
            factory = new ConnectionFactory();
            factory.setHost(operativeBrokerConnParams.getBrokerHost());
            factory.setPassword(operativeBrokerConnParams.getAccessToken());
            factory.setPort(operativeBrokerConnParams.getBrokerPort());
            factory.setUsername(operativeBrokerConnParams.getSubjectID());
            factory.setVirtualHost(operativeBrokerConnParams.getVirtualHost());
            useENSBrokerConnectionParameters(operativeBrokerConnParams);
            try {
                connection = factory.newConnection();
            } catch (IOException e) {
                ENSClientException ce = new ENSClientException(
                        ENSClientExceptionCodes.OPERATIVE_CONNECTION_ERROR,e,
                        factory.getVirtualHost(),factory.getHost());
                LOGGER.log(Level.SEVERE, ce.getMessage(), e);
                throw ce;
            }
        } else {
            FailureResponseType failure = responseObject.getResultDetails().getFailureResponse();
            ENSClientException failureException = 
                    new ENSClientException(failure);
            if (LOGGER.isLoggable(Level.SEVERE)) {
                LOGGER.log(Level.SEVERE, "authorisation.step6FAILURE",
                    new String[]{failure.getErrorCode(),failure.getErrorReason()});
            }
            throw failureException;
        }
    }
    
    /* (non-Javadoc)
     * @see it.txt.ens.client.core.ENSClient#disconnect()
     */
    @Override
    public void disconnect() {
        closeConnection();
    }
    
    protected void closeConnection () {
        try {
            if (isConnected()) {
                connection.close();
            }
        } catch (IOException e) {
            if (LOGGER.isLoggable(Level.WARNING)) {
                LOGGER.log(Level.WARNING, "exceptionWhileClosingConnection",
                    connection.getAddress() + ":" + connection.getPort());
            }
        }
    }
    
    /**
     * Uses the connection parameters according to the kind of ENS client<br/>Subclasses should use this
     * method to retrieve from the connection parameters above  
     * 
     * @param parameters the operative broker's connection parameters.
     */
    protected abstract void useENSBrokerConnectionParameters (ENSBrokerConnectionParameters parameters);
        
    public boolean isConnected () {
        if (connection == null)
            return false;
        return connection.isOpen();
    }
    
}
